2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 #import "AIMessageViewController.h"
18 #import "AIAccountSelectionView.h"
19 #import "AIMessageWindowController.h"
20 #import "ESGeneralPreferencesPlugin.h"
21 #import "AIDualWindowInterfacePlugin.h"
22 #import "AIContactInfoWindowController.h"
23 #import "AIMessageTabSplitView.h"
25 #import <Adium/AIChatControllerProtocol.h>
26 #import <Adium/AIContactAlertsControllerProtocol.h>
27 #import <Adium/AIContactControllerProtocol.h>
28 #import <Adium/AIContentControllerProtocol.h>
29 #import <Adium/AIContentControllerProtocol.h>
30 #import <Adium/AIInterfaceControllerProtocol.h>
31 #import <Adium/AIMenuControllerProtocol.h>
32 #import <Adium/AIPreferenceControllerProtocol.h>
33 #import <Adium/AIToolbarControllerProtocol.h>
34 #import <Adium/AIAccount.h>
35 #import <Adium/AIChat.h>
36 #import <Adium/AIContentMessage.h>
37 #import <Adium/AIListContact.h>
38 #import <Adium/AIListObject.h>
39 #import <Adium/AIListOutlineView.h>
40 #import <Adium/AIMessageEntryTextView.h>
41 #import <Adium/ESTextAndButtonsWindowController.h>
43 #import <AIUtilities/AIApplicationAdditions.h>
44 #import <AIUtilities/AIAttributedStringAdditions.h>
45 #import <AIUtilities/AIAutoScrollView.h>
46 #import <AIUtilities/AIDictionaryAdditions.h>
47 #import <AIUtilities/AISplitView.h>
49 #import <AIUtilities/AITigerCompatibility.h>
51 #import <PSMTabBarControl/NSBezierPath_AMShading.h>
52 #import "KNShelfSplitView.h"
53 #import "ESChatUserListController.h"
56 #define MESSAGE_VIEW_MIN_HEIGHT_RATIO .50 //Mininum height ratio of the message view
57 #define MESSAGE_VIEW_MIN_WIDTH_RATIO .50 //Mininum width ratio of the message view
58 #define ENTRY_TEXTVIEW_MIN_HEIGHT 20 //Mininum height of the text entry view
59 #define USER_LIST_MIN_WIDTH 24 //Mininum width of the user list
60 #define USER_LIST_DEFAULT_WIDTH 120 //Default width of the user list
62 //Preferences and files
63 #define MESSAGE_VIEW_NIB @"MessageView" //Filename of the message view nib
64 #define USERLIST_THEME @"UserList Theme" //File name of the user list theme
65 #define USERLIST_LAYOUT @"UserList Layout" //File name of the user list layout
66 #define KEY_ENTRY_TEXTVIEW_MIN_HEIGHT @"Minimum Text Height" //Preference key for text entry height
67 #define KEY_ENTRY_USER_LIST_MIN_WIDTH @"UserList Width" //Preference key for user list width
70 @interface AIMessageViewController (PRIVATE)
71 - (id)initForChat:(AIChat *)inChat;
72 - (void)chatStatusChanged:(NSNotification *)notification;
73 - (void)chatParticipatingListObjectsChanged:(NSNotification *)notification;
74 - (void)_configureMessageDisplay;
75 - (void)_createAccountSelectionView;
76 - (void)_destroyAccountSelectionView;
77 - (void)_configureTextEntryView;
78 - (void)_updateTextEntryViewHeight;
79 - (int)_textEntryViewProperHeightIgnoringUserMininum:(BOOL)ignoreUserMininum;
80 - (void)_showUserListView;
81 - (void)_hideUserListView;
82 - (void)_configureUserList;
83 - (void)_updateUserListViewWidth;
84 - (int)_userListViewProperWidthIgnoringUserMininum:(BOOL)ignoreUserMininum;
85 - (void)updateFramesForAccountSelectionView;
86 - (void)saveUserListMinimumSize;
89 @implementation AIMessageViewController
92 * @brief Create a new message view controller
94 + (AIMessageViewController *)messageDisplayControllerForChat:(AIChat *)inChat
96 return [[[self alloc] initForChat:inChat] autorelease];
103 - (id)initForChat:(AIChat *)inChat
105 if ((self = [super init])) {
106 AIListContact *contact;
108 chat = [inChat retain];
109 contact = [chat listObject];
110 view_accountSelection = nil;
111 userListController = nil;
112 suppressSendLaterPrompt = NO;
113 retainingScrollViewUserList = NO;
115 //Load the view containing our controls
116 [NSBundle loadNibNamed:MESSAGE_VIEW_NIB owner:self];
118 //Register for the various notification we need
119 [[adium notificationCenter] addObserver:self
120 selector:@selector(sendMessage:)
121 name:Interface_SendEnteredMessage
123 [[adium notificationCenter] addObserver:self
124 selector:@selector(didSendMessage:)
125 name:Interface_DidSendEnteredMessage
127 [[adium notificationCenter] addObserver:self
128 selector:@selector(chatStatusChanged:)
129 name:Chat_StatusChanged
131 [[adium notificationCenter] addObserver:self
132 selector:@selector(chatParticipatingListObjectsChanged:)
133 name:Chat_ParticipatingListObjectsChanged
135 [[adium notificationCenter] addObserver:self
136 selector:@selector(redisplaySourceAndDestinationSelector:)
137 name:Chat_SourceChanged
139 [[adium notificationCenter] addObserver:self
140 selector:@selector(redisplaySourceAndDestinationSelector:)
141 name:Chat_DestinationChanged
143 [[adium notificationCenter] addObserver:self
144 selector:@selector(toggleUserlist:)
145 name:@"toggleUserlist"
148 [splitView_textEntryHorizontal setDividerThickness:3]; //Default is 9
149 [splitView_textEntryHorizontal setDrawsDivider:NO];
151 //Observe general preferences for sending keys
152 [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_GENERAL];
154 /* Update chat status and participating list objects to configure the user list if necessary
155 * Call chatParticipatingListObjectsChanged first, which will set up the user list. This allows other sizing to match.
157 [self setUserListVisible:[chat isGroupChat]];
159 [self chatParticipatingListObjectsChanged:nil];
160 [self chatStatusChanged:nil];
162 //Configure our views
163 [self _configureMessageDisplay];
164 [self _configureTextEntryView];
166 //Set our base writing direction
168 [textView_outgoing setBaseWritingDirection:[contact baseWritingDirection]];
180 AIListContact *contact = [chat listObject];
182 [[adium preferenceController] unregisterPreferenceObserver:self];
184 //Store our minimum height for the text entry area, and minimim width for the user list
185 [[adium preferenceController] setPreference:[NSNumber numberWithInt:entryMinHeight]
186 forKey:KEY_ENTRY_TEXTVIEW_MIN_HEIGHT
187 group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
189 if (userListController) {
190 [self saveUserListMinimumSize];
193 //Save the base writing direction
195 [contact setBaseWritingDirection:[textView_outgoing baseWritingDirection]];
197 [chat release]; chat = nil;
200 [[adium notificationCenter] removeObserver:self];
201 [[NSNotificationCenter defaultCenter] removeObserver:self];
203 //Account selection view
204 [self _destroyAccountSelectionView];
206 [messageDisplayController messageViewIsClosing];
207 [messageDisplayController release];
208 [userListController release];
210 [controllerView_messages release];
212 //Release view_contents, for which we are responsible because we loaded it via -[NSBundle loadNibNamed:owner]
213 [view_contents release];
215 //Release the hidden user list view
216 if (retainingScrollViewUserList) {
217 [scrollView_userList release];
224 - (void)saveUserListMinimumSize
226 [[adium preferenceController] setPreference:[NSNumber numberWithInt:userListMinWidth]
227 forKey:KEY_ENTRY_USER_LIST_MIN_WIDTH
228 group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
231 - (void)updateGradientColors
233 NSColor *darkerColor = [NSColor colorWithCalibratedWhite:0.90 alpha:1.0];
234 NSColor *lighterColor = [NSColor colorWithCalibratedWhite:0.92 alpha:1.0];
235 NSColor *leftColor = nil, *rightColor = nil;
237 switch ([messageWindowController tabPosition]) {
238 case AdiumTabPositionBottom:
239 case AdiumTabPositionTop:
240 case AdiumTabPositionLeft:
241 leftColor = lighterColor;
242 rightColor = darkerColor;
244 case AdiumTabPositionRight:
245 leftColor = darkerColor;
246 rightColor = lighterColor;
250 [view_accountSelection setLeftColor:leftColor rightColor:rightColor];
251 [splitView_textEntryHorizontal setLeftColor:leftColor rightColor:rightColor];
255 * @brief Invoked before the message view closes
257 * This method is invoked before our message view controller's message view leaves a window.
258 * We need to clean up our user list to invalidate cursor tracking before the view closes.
260 - (void)messageViewWillLeaveWindowController:(AIMessageWindowController *)inWindowController
262 if (inWindowController) {
263 [userListController contactListWillBeRemovedFromWindow];
266 [messageWindowController release]; messageWindowController = nil;
269 - (void)messageViewAddedToWindowController:(AIMessageWindowController *)inWindowController
271 if (inWindowController) {
272 [userListController contactListWasAddedBackToWindow];
275 if (inWindowController != messageWindowController) {
276 [messageWindowController release];
277 messageWindowController = [inWindowController retain];
279 [self updateGradientColors];
284 * @brief Retrieve the chat represented by this message view
292 * @brief Retrieve the source account associated with this chat
294 - (AIAccount *)account
296 return [chat account];
300 * @brief Retrieve the destination list object associated with this chat
302 - (AIListContact *)listObject
304 return [chat listObject];
308 * @brief Returns the selected list object in our participants list
310 - (AIListObject *)preferredListObject
312 if (userListView) { //[[shelfView subviews] containsObject:scrollView_userList] && ([userListView selectedRow] != -1)
313 return [userListView itemAtRow:[userListView selectedRow]];
320 * @brief Invoked when the status of our chat changes
322 * The only chat status change we're interested in is one to the disallow account switching flag. When this flag
323 * changes we update the visibility of our account status menus accordingly.
325 - (void)chatStatusChanged:(NSNotification *)notification
327 NSArray *modifiedKeys = [[notification userInfo] objectForKey:@"Keys"];
329 if (notification == nil || [modifiedKeys containsObject:@"DisallowAccountSwitching"]) {
330 [self setAccountSelectionMenuVisibleIfNeeded:YES];
335 //Message Display ------------------------------------------------------------------------------------------------------
336 #pragma mark Message Display
338 * @brief Configure the message display view
340 - (void)_configureMessageDisplay
342 //Create the message view
343 messageDisplayController = [[[adium interfaceController] messageDisplayControllerForChat:chat] retain];
344 //Get the messageView from the controller
345 controllerView_messages = [[messageDisplayController messageView] retain];
346 //scrollView_messages is originally a placeholder; replace it with controllerView_messages
347 [controllerView_messages setFrame:[scrollView_messages documentVisibleRect]];
348 [[customView_messages superview] replaceSubview:customView_messages with:controllerView_messages];
350 //This is what draws our transparent background
351 //Technically, it could be set in MessageView.nib, too
352 [scrollView_messages setBackgroundColor:[NSColor clearColor]];
354 [controllerView_messages setNextResponder:textView_outgoing];
358 * @brief Access to our view
362 return view_contents;
366 * @brief Support for printing. Forward the print command to our message display view
368 - (void)adiumPrint:(id)sender
370 if ([messageDisplayController respondsToSelector:@selector(adiumPrint:)]) {
371 [messageDisplayController adiumPrint:sender];
376 //Messaging ------------------------------------------------------------------------------------------------------------
377 #pragma mark Messaging
379 * @brief Send the entered message
381 - (IBAction)sendMessage:(id)sender
383 NSAttributedString *attributedString = [textView_outgoing textStorage];
385 //Only send if we have a non-zero-length string
386 if ([attributedString length] != 0) {
387 AIListObject *listObject = [chat listObject];
389 if ([chat isGroupChat] && ![[chat account] online]) {
390 //Refuse to do anything with a group chat for an offline account.
395 if (!suppressSendLaterPrompt &&
396 ![chat canSendMessages]) {
398 NSString *formattedUID = [listObject formattedUID];
400 NSAlert *alert = [[NSAlert alloc] init];
401 [alert setMessageText:[NSString stringWithFormat:AILocalizedString(@"%@ appears to be offline. How do you want to send this message?", nil),
403 [alert setInformativeText:[NSString stringWithFormat:
404 AILocalizedString(@"Send Later will send the message the next time both you and %@ are online. Send Now may work if %@ is invisible or is not on your contact list and so only appears to be offline.", "Send Later dialogue explanation text"),
405 formattedUID, formattedUID, formattedUID]];
406 [alert addButtonWithTitle:AILocalizedString(@"Send Now", nil)];
408 [alert addButtonWithTitle:AILocalizedString(@"Send Later", nil)];
409 [[[alert buttons] objectAtIndex:1] setKeyEquivalent:@"l"];
410 [[[alert buttons] objectAtIndex:1] setKeyEquivalentModifierMask:0];
412 [alert addButtonWithTitle:AILocalizedString(@"Don't Send", nil)];
413 [[[alert buttons] objectAtIndex:2] setKeyEquivalent:@"\E"];
414 [[[alert buttons] objectAtIndex:2] setKeyEquivalentModifierMask:0];
416 NSImage *icon = ([listObject userIcon] ? [listObject userIcon] : [AIServiceIcons serviceIconForObject:listObject
417 type:AIServiceIconLarge
418 direction:AIIconNormal]);
419 icon = [[icon copy] autorelease];
420 [icon setScalesWhenResized:NO];
421 [alert setIcon:icon];
422 [alert setAlertStyle:NSInformationalAlertStyle];
424 [alert beginSheetModalForWindow:[view_contents window]
426 didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:)
432 AIContentMessage *message;
433 NSAttributedString *outgoingAttributedString;
434 AIAccount *account = [chat account];
436 [[adium notificationCenter] postNotificationName:Interface_WillSendEnteredMessage
440 outgoingAttributedString = [attributedString copy];
441 message = [AIContentMessage messageInChat:chat
443 destination:[chat listObject]
444 date:nil //created for us by AIContentMessage
445 message:outgoingAttributedString
447 [outgoingAttributedString release];
449 if ([[adium contentController] sendContentObject:message]) {
450 [[adium notificationCenter] postNotificationName:Interface_DidSendEnteredMessage
459 * @brief Send Later button was pressed
461 - (void)alertDidEnd:(NSAlert *)alert returnCode:(int)returnCode contextInfo:(void *)contextInfo
463 switch (returnCode) {
464 case NSAlertFirstButtonReturn: /* Send Now */
465 suppressSendLaterPrompt = YES;
466 [self sendMessage:nil];
469 case NSAlertSecondButtonReturn: /* Send Later */
470 [self sendMessageLater:nil];
472 case NSAlertThirdButtonReturn: /* Don't Send */
478 * @brief Invoked after our entered message sends
480 * This method hides the account selection view and clears the entered message after our message sends
482 - (IBAction)didSendMessage:(id)sender
484 [self setAccountSelectionMenuVisibleIfNeeded:NO];
485 [self clearTextEntryView];
487 //Redisplay the cursor
488 [NSCursor setHiddenUntilMouseMoves:NO];
492 * @brief Offline messaging
494 - (IBAction)sendMessageLater:(id)sender
496 AIListContact *listContact;
498 //If the chat can _now_ send a message, send it immediately instead of waiting for "later".
499 if ([chat canSendMessages]) {
500 [self sendMessage:sender];
504 //Put the alert on the metaContact containing this listContact if applicable
505 listContact = [[chat listObject] parentContact];
508 NSMutableDictionary *detailsDict, *alertDict;
510 detailsDict = [NSMutableDictionary dictionary];
511 [detailsDict setObject:[[chat account] internalObjectID] forKey:@"Account ID"];
512 [detailsDict setObject:[NSNumber numberWithBool:YES] forKey:@"Allow Other"];
513 [detailsDict setObject:[listContact internalObjectID] forKey:@"Destination ID"];
515 alertDict = [NSMutableDictionary dictionary];
516 [alertDict setObject:detailsDict forKey:@"ActionDetails"];
517 [alertDict setObject:CONTACT_SEEN_ONLINE_YES forKey:@"EventID"];
518 [alertDict setObject:@"SendMessage" forKey:@"ActionID"];
519 [alertDict setObject:[NSNumber numberWithBool:YES] forKey:@"OneTime"];
521 [alertDict setObject:listContact forKey:@"TEMP-ListContact"];
523 [[adium contentController] filterAttributedString:[[[textView_outgoing textStorage] copy] autorelease]
524 usingFilterType:AIFilterContent
525 direction:AIFilterOutgoing
526 filterContext:listContact
528 selector:@selector(gotFilteredMessageToSendLater:receivingContext:)
531 [self didSendMessage:nil];
536 * @brief Offline messaging
538 //XXX - Offline messaging code SHOULD NOT BE IN HERE! -ai
539 - (void)gotFilteredMessageToSendLater:(NSAttributedString *)filteredMessage receivingContext:(NSMutableDictionary *)alertDict
541 NSMutableDictionary *detailsDict;
542 AIListContact *listContact;
544 detailsDict = [alertDict objectForKey:@"ActionDetails"];
545 [detailsDict setObject:[filteredMessage dataRepresentation] forKey:@"Message"];
547 listContact = [[alertDict objectForKey:@"TEMP-ListContact"] retain];
548 [alertDict removeObjectForKey:@"TEMP-ListContact"];
550 [[adium contactAlertsController] addAlert:alertDict
551 toListObject:listContact
552 setAsNewDefaults:NO];
553 [listContact release];
556 //Account Selection ----------------------------------------------------------------------------------------------------
557 #pragma mark Account Selection
561 - (void)accountSelectionViewFrameDidChange:(NSNotification *)notification
563 [self updateFramesForAccountSelectionView];
567 * @brief Redisplay the source/destination account selector
569 - (void)redisplaySourceAndDestinationSelector:(NSNotification *)notification
571 [self setAccountSelectionMenuVisibleIfNeeded:YES];
575 * @brief Toggle visibility of the account selection menus
577 * Invoking this method with NO will hide the account selection menus. Invoking it with YES will show the account
578 * selection menus if they are needed.
580 - (void)setAccountSelectionMenuVisibleIfNeeded:(BOOL)makeVisible
582 //Hide or show the account selection view as requested
584 [self _createAccountSelectionView];
586 [self _destroyAccountSelectionView];
591 * @brief Show the account selection view
593 - (void)_createAccountSelectionView
595 if (!view_accountSelection) {
596 NSRect contentFrame = [splitView_textEntryHorizontal frame];
598 //Create the account selection view and insert it into our window
599 view_accountSelection = [[AIAccountSelectionView alloc] initWithFrame:contentFrame];
601 [view_accountSelection setAutoresizingMask:(NSViewWidthSizable | NSViewMinYMargin)];
603 [self updateGradientColors];
605 //Insert the account selection view at the top of our view
606 [[shelfView contentView] addSubview:view_accountSelection];
607 [view_accountSelection setChat:chat];
609 [[NSNotificationCenter defaultCenter] addObserver:self
610 selector:@selector(accountSelectionViewFrameDidChange:)
611 name:AIViewFrameDidChangeNotification
612 object:view_accountSelection];
614 [self updateFramesForAccountSelectionView];
616 //Redisplay everything
617 [[shelfView contentView] setNeedsDisplay:YES];
622 * @brief Hide the account selection view
624 - (void)_destroyAccountSelectionView
626 if (view_accountSelection) {
627 //Remove the observer
628 [[NSNotificationCenter defaultCenter] removeObserver:self
629 name:AIViewFrameDidChangeNotification
630 object:view_accountSelection];
632 //Remove the account selection view from our window, clean it up
633 [view_accountSelection removeFromSuperview];
634 [view_accountSelection release]; view_accountSelection = nil;
636 //Redisplay everything
637 [self updateFramesForAccountSelectionView];
642 * @brief Position the account selection view, if it is present, and the messages/text entry splitview appropriately
644 - (void)updateFramesForAccountSelectionView
646 int contentsHeight = [[shelfView contentView] frame].size.height;
647 int accountSelectionHeight = (view_accountSelection ? [view_accountSelection frame].size.height : 0);
648 int intersectionPoint = ([[shelfView contentView] isFlipped] ? accountSelectionHeight : (contentsHeight - accountSelectionHeight));
650 if (view_accountSelection) {
651 [view_accountSelection setFrameOrigin:NSMakePoint(NSMinX([view_accountSelection frame]), intersectionPoint)];
652 [view_accountSelection setNeedsDisplay:YES];
655 [splitView_textEntryHorizontal setFrameSize:NSMakeSize(NSWidth([splitView_textEntryHorizontal frame]), intersectionPoint)];
656 [splitView_textEntryHorizontal setNeedsDisplay:YES];
660 //Text Entry -----------------------------------------------------------------------------------------------------------
661 #pragma mark Text Entry
663 * @brief Preferences changed, update sending keys
665 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key object:(AIListObject *)object
666 preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
668 [textView_outgoing setSendOnReturn:[[prefDict objectForKey:SEND_ON_RETURN] boolValue]];
669 [textView_outgoing setSendOnEnter:[[prefDict objectForKey:SEND_ON_ENTER] boolValue]];
673 * @brief Configure the text entry view
675 - (void)_configureTextEntryView
677 //Configure the text entry view
678 [textView_outgoing setTarget:self action:@selector(sendMessage:)];
680 //This is necessary for tab completion.
681 [textView_outgoing setDelegate:self];
683 [textView_outgoing setTextContainerInset:NSMakeSize(0,2)];
684 if ([textView_outgoing respondsToSelector:@selector(setUsesFindPanel:)]) {
685 [textView_outgoing setUsesFindPanel:YES];
687 [textView_outgoing setClearOnEscape:YES];
688 [textView_outgoing setTypingAttributes:[[adium contentController] defaultFormattingAttributes]];
690 //User's choice of mininum height for their text entry view
691 entryMinHeight = [[[adium preferenceController] preferenceForKey:KEY_ENTRY_TEXTVIEW_MIN_HEIGHT
692 group:PREF_GROUP_DUAL_WINDOW_INTERFACE] intValue];
693 if (entryMinHeight <= 0) entryMinHeight = [self _textEntryViewProperHeightIgnoringUserMininum:YES];
695 //Associate the view with our message view so it knows which view to scroll in response to page up/down
696 //and other special key-presses.
697 [textView_outgoing setAssociatedView:[messageDisplayController messageScrollView]];
699 //Associate the text entry view with our chat and inform Adium that it exists.
700 //This is necessary for text entry filters to work correctly.
701 [textView_outgoing setChat:chat];
703 //Observe text entry view size changes so we can dynamically resize as the user enters text
704 [[NSNotificationCenter defaultCenter] addObserver:self
705 selector:@selector(outgoingTextViewDesiredSizeDidChange:)
706 name:AIViewDesiredSizeDidChangeNotification
707 object:textView_outgoing];
709 [self _updateTextEntryViewHeight];
713 * @brief Sets our text entry view as the first responder
715 - (void)makeTextEntryViewFirstResponder
717 [[textView_outgoing window] makeFirstResponder:textView_outgoing];
721 * @brief Clear the message entry text view
723 - (void)clearTextEntryView
725 NSWritingDirection writingDirection;
727 writingDirection = [textView_outgoing baseWritingDirection];
729 [textView_outgoing setString:@""];
730 [textView_outgoing setTypingAttributes:[[adium contentController] defaultFormattingAttributes]];
732 [textView_outgoing setBaseWritingDirection:writingDirection]; //Preserve the writing diraction
734 [[NSNotificationCenter defaultCenter] postNotificationName:NSTextDidChangeNotification
735 object:textView_outgoing];
739 * @brief Add text to the message entry text view
741 * Adds the passed string to the entry text view at the insertion point. If there is selected text in the view, it
744 - (void)addToTextEntryView:(NSAttributedString *)inString
746 [textView_outgoing insertText:inString];
747 [[NSNotificationCenter defaultCenter] postNotificationName:NSTextDidChangeNotification object:textView_outgoing];
751 * @brief Add data to the message entry text view
753 * Adds the passed pasteboard data to the entry text view at the insertion point. If there is selected text in the
754 * view, it will be replaced.
756 - (void)addDraggedDataToTextEntryView:(id <NSDraggingInfo>)draggingInfo
758 [textView_outgoing performDragOperation:draggingInfo];
759 [[NSNotificationCenter defaultCenter] postNotificationName:NSTextDidChangeNotification object:textView_outgoing];
763 * @brief Update the text entry view's height when its desired size changes
765 - (void)outgoingTextViewDesiredSizeDidChange:(NSNotification *)notification
767 [self _updateTextEntryViewHeight];
770 - (void)tabViewDidChangeVisibility
772 [self _updateTextEntryViewHeight];
776 * @brief Update the height of our text entry view
778 * This method sets the height of the text entry view to the most ideal value, and adjusts the other views in our
779 * window to fill the remaining space.
781 - (void)_updateTextEntryViewHeight
783 int height = [self _textEntryViewProperHeightIgnoringUserMininum:NO];
785 //Display the vertical scroller if our view is not tall enough to display all the entered text
786 [scrollView_outgoing setHasVerticalScroller:(height < [textView_outgoing desiredSize].height)];
788 if ([NSApp isOnLeopardOrBetter]) {
789 //Attempt to maximize the message view's size. We'll automatically restrict it to the correct minimum via the NSSplitView's delegate methods.
790 [splitView_textEntryHorizontal setPosition:NSHeight([splitView_textEntryHorizontal frame])
794 NSRect tempFrame, newFrame;
797 //Size the outgoing text view to the desired height
798 tempFrame = [scrollView_outgoing frame];
799 newFrame = NSMakeRect(tempFrame.origin.x,
800 [splitView_textEntryHorizontal frame].size.height - height,
801 tempFrame.size.width,
803 if (!NSEqualRects(tempFrame, newFrame)) {
804 [scrollView_outgoing setFrame:newFrame];
805 [scrollView_outgoing setNeedsDisplay:YES];
810 [splitView_textEntryHorizontal adjustSubviews];
816 * @brief Returns the height our text entry view should be
818 * This method takes into account user preference, the amount of entered text, and the current window size to return
819 * a height which is most ideal for the text entry view.
821 * @param ignoreUserMininum If YES, the user's preference for mininum height will be ignored
823 - (int)_textEntryViewProperHeightIgnoringUserMininum:(BOOL)ignoreUserMininum
825 int dividerThickness = [splitView_textEntryHorizontal dividerThickness];
826 int allowedHeight = ([splitView_textEntryHorizontal frame].size.height / 2.0) - dividerThickness;
829 //Our primary goal is to display all the entered text
830 height = [textView_outgoing desiredSize].height;
832 //But we must never fall below the user's prefered mininum or above the allowed height
833 if (!ignoreUserMininum && height < entryMinHeight) height = entryMinHeight;
834 if (height > allowedHeight) height = allowedHeight;
839 #pragma mark Autocompletion
841 * @brief Should the tab key cause an autocompletion if possible?
843 * We only tab to autocomplete for a group chat
845 - (BOOL)textViewShouldTabComplete:(NSTextView *)inTextView
847 return [[self chat] isGroupChat];
850 - (NSArray *)textView:(NSTextView *)textView completions:(NSArray *)words forPartialWordRange:(NSRange)charRange indexOfSelectedItem:(int *)index
852 NSMutableArray *completions;
854 if ([[self chat] isGroupChat]) {
855 NSString *partialWord = [[[textView textStorage] attributedSubstringFromRange:charRange] string];
856 NSEnumerator *enumerator;
857 AIListContact *listContact;
860 if (charRange.location == 0) {
861 //At the start of a line, append ": "
867 completions = [NSMutableArray array];
868 enumerator = [[[self chat] containedObjects] objectEnumerator];
869 while ((listContact = [enumerator nextObject])) {
870 if ([[listContact displayName] rangeOfString:partialWord
871 options:(NSLiteralSearch | NSAnchoredSearch)].location != NSNotFound) {
873 [completions addObject:(suffix ? [[listContact displayName] stringByAppendingString:suffix] : [listContact displayName])];
875 } else if ([[listContact formattedUID] rangeOfString:partialWord
876 options:(NSLiteralSearch | NSAnchoredSearch)].location != NSNotFound) {
877 [completions addObject:(suffix ? [[listContact formattedUID] stringByAppendingString:suffix] : [listContact formattedUID])];
879 } else if ([[listContact UID] rangeOfString:partialWord
880 options:(NSLiteralSearch | NSAnchoredSearch)].location != NSNotFound) {
881 [completions addObject:(suffix ? [[listContact UID] stringByAppendingString:suffix] : [listContact UID])];
885 if ([completions count]) {
893 return ([completions count] ? completions : words);
896 //User List ------------------------------------------------------------------------------------------------------------
897 #pragma mark User List
899 * @brief Set visibility of the user list
901 - (void)setUserListVisible:(BOOL)inVisible
904 [self _showUserListView];
906 [self _hideUserListView];
911 * @brief Returns YES if the user list is currently visible
913 - (BOOL)userListVisible
915 return [shelfView isShelfVisible];
919 * @brief Show the user list
921 - (void)_showUserListView
923 [self setupShelfView];
925 //Configure the user list
926 [self _configureUserList];
928 //Add the user list back to our window if it's missing
929 if (![self userListVisible]) {
930 [self _updateUserListViewWidth];
932 if (retainingScrollViewUserList) {
933 [scrollView_userList release];
934 retainingScrollViewUserList = NO;
940 * @brief Hide the user list.
942 * We gain responsibility for releasing scrollView_userList after we hide it
944 - (void)_hideUserListView
946 if ([self userListVisible]) {
947 [scrollView_userList retain];
948 [scrollView_userList removeFromSuperview];
949 retainingScrollViewUserList = YES;
951 [self saveUserListMinimumSize];
952 [userListController release];
953 userListController = nil;
955 //need to collapse the splitview
956 [shelfView setShelfIsVisible:NO];
961 * @brief Configure the user list
963 * Configures the user list view and prepares it for display. If the user list is not being shown, this configuration
964 * should be avoided for performance.
966 - (void)_configureUserList
968 if (!userListController) {
969 NSDictionary *themeDict = [NSDictionary dictionaryNamed:USERLIST_THEME forClass:[self class]];
970 NSDictionary *layoutDict = [NSDictionary dictionaryNamed:USERLIST_LAYOUT forClass:[self class]];
972 //Create and configure a controller to manage the user list
973 userListController = [[ESChatUserListController alloc] initWithContactListView:userListView
974 inScrollView:scrollView_userList
976 [userListController updateLayoutFromPrefDict:layoutDict andThemeFromPrefDict:themeDict];
977 [userListController setContactListRoot:chat];
978 [userListController setHideRoot:YES];
980 //User's choice of mininum width for their user list view
981 userListMinWidth = [[[adium preferenceController] preferenceForKey:KEY_ENTRY_USER_LIST_MIN_WIDTH
982 group:PREF_GROUP_DUAL_WINDOW_INTERFACE] intValue];
983 if (userListMinWidth < USER_LIST_MIN_WIDTH) userListMinWidth = USER_LIST_DEFAULT_WIDTH;
984 [shelfView setShelfWidth:[userListView bounds].size.width];
989 * @brief Update the user list in response to changes
991 * This method is invoked when the chat's participating contacts change. In resopnse, it sets correct visibility of
992 * the user list, and updates the displayed users.
994 - (void)chatParticipatingListObjectsChanged:(NSNotification *)notification
996 //Update the user list
997 AILogWithSignature(@"%i, so %@ %@",[self userListVisible], ([self userListVisible] ? @"reloading" : @"not reloading"),
999 if ([self userListVisible]) {
1000 [userListController reloadData];
1005 * @brief The selection in the user list changed
1007 * When the user list selection changes, we update the chat's "preferred list object", which is used
1008 * elsewhere to identify the currently 'selected' contact for Get Info, Messaging, etc.
1010 - (void)outlineViewSelectionDidChange:(NSNotification *)notification
1012 if ([notification object] == userListView) {
1013 int selectedIndex = [userListView selectedRow];
1014 [chat setPreferredListObject:((selectedIndex != -1) ?
1015 [[chat containedObjects] objectAtIndex:selectedIndex] :
1021 * @brief Perform default action on the selected user list object
1023 * Here we could open a private message or display info for the user, however we perform no action
1026 - (void)performDefaultActionOnSelectedObject:(AIListObject *)listObject sender:(NSOutlineView *)sender
1032 * @brief Update the width of our user list view
1034 * This method sets the width of the user list view to the most ideal value, and adjusts the other views in our
1035 * window to fill the remaining space.
1037 - (void)_updateUserListViewWidth
1039 int width = [self _userListViewProperWidthIgnoringUserMininum:NO];
1040 int widthWithDivider = 1 + width; //resize bar effective width
1043 //Size the user list view to the desired width
1044 tempFrame = [scrollView_userList frame];
1045 [scrollView_userList setFrame:NSMakeRect([shelfView frame].size.width - width,
1048 tempFrame.size.height)];
1050 //Size the message view to fill the remaining space
1051 tempFrame = [scrollView_messages frame];
1052 [scrollView_messages setFrame:NSMakeRect(tempFrame.origin.x,
1054 [shelfView frame].size.width - widthWithDivider,
1055 tempFrame.size.height)];
1057 //Redisplay both views and the divider
1058 [shelfView setNeedsDisplay:YES];
1062 * @brief Returns the width our user list view should be
1064 * This method takes into account user preference and the current window size to return a width which is most
1065 * ideal for the user list view.
1067 * @param ignoreUserMininum If YES, the user's preference for mininum width will be ignored
1069 - (int)_userListViewProperWidthIgnoringUserMininum:(BOOL)ignoreUserMininum
1071 int dividerThickness = 1; //[shelfView dividerThickness];
1072 int allowedWidth = ([shelfView frame].size.width / 2.0) - dividerThickness;
1073 int width = USER_LIST_MIN_WIDTH;
1075 //We must never fall below the user's prefered mininum or above the allowed width
1076 if (!ignoreUserMininum && width < userListMinWidth) width = userListMinWidth;
1077 if (width > allowedWidth) width = allowedWidth;
1083 //Split Views --------------------------------------------------------------------------------------------------
1084 #pragma mark Split Views
1086 * @brief Returns the maximum constraint of the split pane
1088 * For the horizontal split, we prevent the message view from growing so large that the text entry view
1089 * is forced below its desired height.
1091 - (float)splitView:(NSSplitView *)sender constrainMaxCoordinate:(float)proposedMax ofSubviewAt:(int)offset
1093 if (sender == splitView_textEntryHorizontal) {
1094 return ([sender frame].size.height - ([self _textEntryViewProperHeightIgnoringUserMininum:YES] +
1095 [sender dividerThickness]));
1098 NSLog(@"Unknown split view %@",sender);
1104 * @brief Returns the mininum constraint of the split pane
1106 * For both splitpanes, we prevent the message view from dropping below 50% of the window's width and height
1108 - (float)splitView:(NSSplitView *)sender constrainMinCoordinate:(float)proposedMin ofSubviewAt:(int)offset
1110 if (sender == splitView_textEntryHorizontal) {
1111 return (int)([sender frame].size.height * MESSAGE_VIEW_MIN_HEIGHT_RATIO);
1114 NSLog(@"Unknown split view %@",sender);
1120 * @brief A split view had its divider position changed
1122 * Remember the user's choice of text entry view height.
1124 - (float)splitView:(NSSplitView *)sender constrainSplitPosition:(float)proposedPosition ofSubviewAt:(int)index
1126 if (sender == splitView_textEntryHorizontal) {
1127 entryMinHeight = (int)([sender frame].size.height - (proposedPosition + [sender dividerThickness]));
1129 NSLog(@"Unknown split view %@",sender);
1133 return proposedPosition;
1137 * @brief Returns YES if the passed subview can be collapsed
1139 - (BOOL)splitView:(NSSplitView *)sender canCollapseSubview:(NSView *)subview
1141 if (sender == splitView_textEntryHorizontal) {
1145 NSLog(@"Unknown split view %@",sender);
1150 #pragma mark Shelfview
1151 /* @name setupShelfView
1152 * @brief sets up shelfsplitview containing userlist & contentviews
1154 -(void)setupShelfView
1156 [shelfView setShelfWidth:200];
1158 AILogWithSignature(@"ShelfView %@ (content view is %@) --> superview %@, in window %@; frame %@; content view %@ shelf view %@ in window %@",
1159 shelfView, [shelfView contentView], [shelfView superview], [shelfView window], NSStringFromRect([[shelfView superview] frame]),
1160 splitView_textEntryHorizontal,
1161 scrollView_userList, [scrollView_userList window]);
1163 [shelfView bind:@"contextButtonMenu" toObject:[self chat] withKeyPath:@"actionMenu"
1164 options:[NSDictionary dictionaryWithObjectsAndKeys:
1165 [NSNumber numberWithBool:YES], NSAllowsNullArgumentBindingOption,
1166 [NSNumber numberWithBool:YES], NSValidatesImmediatelyBindingOption,
1168 [shelfView setContextButtonImage:[NSImage imageNamed:@"sidebarActionWidget.png"]];
1170 [shelfView setShelfIsVisible:YES];
1173 /* @name toggleUserlist
1174 * @brief toggles the state of the userlist shelf
1176 -(void)toggleUserlist:(id)sender
1178 [shelfView setShelfIsVisible:![shelfView isShelfVisible]];